/*
 * Copyright (c) 2012-2017 Red Hat, Inc.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *   Red Hat, Inc. - initial API and implementation
 */
package org.eclipse.che.api.project.server;

import static org.eclipse.che.api.project.server.DtoConverter.asDto;
import static org.eclipse.che.api.project.shared.Constants.LINK_REL_CREATE_BATCH_PROJECTS;
import static org.eclipse.che.api.project.shared.Constants.LINK_REL_CREATE_PROJECT;
import static org.eclipse.che.api.project.shared.Constants.LINK_REL_GET_PROJECTS;
import static org.eclipse.che.dto.server.DtoFactory.newDto;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.validation.constraints.NotNull;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import org.apache.commons.fileupload.FileItem;
import org.apache.tika.Tika;
import org.eclipse.che.WorkspaceIdProvider;
import org.eclipse.che.api.core.BadRequestException;
import org.eclipse.che.api.core.ConflictException;
import org.eclipse.che.api.core.ForbiddenException;
import org.eclipse.che.api.core.NotFoundException;
import org.eclipse.che.api.core.ServerException;
import org.eclipse.che.api.core.UnauthorizedException;
import org.eclipse.che.api.core.jsonrpc.commons.JsonRpcException;
import org.eclipse.che.api.core.jsonrpc.commons.RequestHandlerConfigurator;
import org.eclipse.che.api.core.jsonrpc.commons.RequestTransmitter;
import org.eclipse.che.api.core.model.project.type.Value;
import org.eclipse.che.api.core.notification.EventService;
import org.eclipse.che.api.core.rest.Service;
import org.eclipse.che.api.core.rest.annotations.Description;
import org.eclipse.che.api.core.rest.annotations.GenerateLink;
import org.eclipse.che.api.core.util.CompositeLineConsumer;
import org.eclipse.che.api.project.server.importer.ProjectImportOutputJsonRpcLineConsumer;
import org.eclipse.che.api.project.server.importer.ProjectImportOutputJsonRpcRegistrar;
import org.eclipse.che.api.project.server.importer.ProjectImportOutputWSLineConsumer;
import org.eclipse.che.api.project.server.notification.ProjectItemModifiedEvent;
import org.eclipse.che.api.project.server.type.ProjectTypeResolution;
import org.eclipse.che.api.project.shared.dto.CopyOptions;
import org.eclipse.che.api.project.shared.dto.ItemReference;
import org.eclipse.che.api.project.shared.dto.MoveOptions;
import org.eclipse.che.api.project.shared.dto.ProjectSearchRequestDto;
import org.eclipse.che.api.project.shared.dto.ProjectSearchResponseDto;
import org.eclipse.che.api.project.shared.dto.SearchOccurrenceDto;
import org.eclipse.che.api.project.shared.dto.SearchResultDto;
import org.eclipse.che.api.project.shared.dto.SourceEstimation;
import org.eclipse.che.api.project.shared.dto.TreeElement;
import org.eclipse.che.api.vfs.VirtualFile;
import org.eclipse.che.api.vfs.search.QueryExpression;
import org.eclipse.che.api.vfs.search.SearchResult;
import org.eclipse.che.api.vfs.search.SearchResultEntry;
import org.eclipse.che.api.vfs.search.Searcher;
import org.eclipse.che.api.vfs.search.impl.LuceneSearcher;
import org.eclipse.che.api.workspace.shared.dto.NewProjectConfigDto;
import org.eclipse.che.api.workspace.shared.dto.ProjectConfigDto;
import org.eclipse.che.api.workspace.shared.dto.SourceStorageDto;
import org.eclipse.che.commons.env.EnvironmentContext;
import org.eclipse.che.commons.lang.ws.rs.ExtMediaType;
import org.eclipse.che.dto.server.DtoFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Project API.
 *
 * @author andrew00x
 * @author Eugene Voevodin
 * @author Artem Zatsarynnyi
 * @author Valeriy Svydenko
 * @author Dmitry Shnurenko
 */
@Api(value = "/project", description = "Project REST API")
@Path("/project")
@Singleton
public class ProjectService extends Service {
  private static final Logger LOG = LoggerFactory.getLogger(ProjectService.class);
  private static Tika TIKA;

  private final ProjectManager projectManager;
  private final EventService eventService;
  private final ProjectServiceLinksInjector projectServiceLinksInjector;
  private final ProjectServiceVcsStatusInjector vcsStatusInjector;
  private final RequestTransmitter transmitter;
  private final ProjectImportOutputJsonRpcRegistrar projectImportHandlerRegistrar;
  private final String workspace;

  @Inject
  public ProjectService(
      ProjectManager projectManager,
      EventService eventService,
      ProjectServiceLinksInjector projectServiceLinksInjector,
      ProjectServiceVcsStatusInjector vcsStatusInjector,
      RequestTransmitter transmitter,
      ProjectImportOutputJsonRpcRegistrar projectImportHandlerRegistrar) {
    this.projectManager = projectManager;
    this.eventService = eventService;
    this.projectServiceLinksInjector = projectServiceLinksInjector;
    this.vcsStatusInjector = vcsStatusInjector;
    this.transmitter = transmitter;
    this.projectImportHandlerRegistrar = projectImportHandlerRegistrar;
    this.workspace = WorkspaceIdProvider.getWorkspaceId();
  }

  @GET
  @Produces(MediaType.APPLICATION_JSON)
  @ApiOperation(
    value = "Gets list of projects in root folder",
    response = ProjectConfigDto.class,
    responseContainer = "List"
  )
  @ApiResponses({
    @ApiResponse(code = 200, message = "OK"),
    @ApiResponse(code = 500, message = "Server error")
  })
  @GenerateLink(rel = LINK_REL_GET_PROJECTS)
  public List<ProjectConfigDto> getProjects()
      throws IOException, ServerException, ConflictException, ForbiddenException {
    return projectManager
        .getProjects()
        .stream()
        .map(p -> injectProjectLinks(asDto(p)))
        .collect(Collectors.toList());
  }

  @GET
  @Path("/{path:.*}")
  @Produces(MediaType.APPLICATION_JSON)
  @ApiOperation(
    value = "Gets project by ID of workspace and project's path",
    response = ProjectConfigDto.class
  )
  @ApiResponses({
    @ApiResponse(code = 200, message = "OK"),
    @ApiResponse(code = 404, message = "Project with specified path doesn't exist in workspace"),
    @ApiResponse(code = 403, message = "Access to requested project is forbidden"),
    @ApiResponse(code = 500, message = "Server error")
  })
  public ProjectConfigDto getProject(
      @ApiParam(value = "Path to requested project", required = true) @PathParam("path")
          String path)
      throws NotFoundException, ForbiddenException, ServerException, ConflictException {
    return injectProjectLinks(asDto(projectManager.getProject(path)));
  }

  @POST
  @Consumes(MediaType.APPLICATION_JSON)
  @Produces(MediaType.APPLICATION_JSON)
  @ApiOperation(value = "Creates new project", response = ProjectConfigDto.class)
  @ApiResponses({
    @ApiResponse(code = 200, message = "OK"),
    @ApiResponse(code = 403, message = "Operation is forbidden"),
    @ApiResponse(code = 409, message = "Project with specified name already exist in workspace"),
    @ApiResponse(code = 500, message = "Server error")
  })
  @GenerateLink(rel = LINK_REL_CREATE_PROJECT)
  /** NOTE: parentPath is added to make a module */
  public ProjectConfigDto createProject(
      @ApiParam(value = "Add to this project as module", required = false) @Context UriInfo uriInfo,
      @Description("descriptor of project") ProjectConfigDto projectConfig)
      throws ConflictException, ForbiddenException, ServerException, NotFoundException {
    Map<String, String> options = new HashMap<>();
    MultivaluedMap<String, String> map = uriInfo.getQueryParameters();
    for (String key : map.keySet()) {
      options.put(key, map.get(key).get(0));
    }
    String pathToProject = projectConfig.getPath();
    String pathToParent = pathToProject.substring(0, pathToProject.lastIndexOf("/"));

    if (!pathToParent.equals("/")) {
      VirtualFileEntry parentFileEntry = projectManager.getProjectsRoot().getChild(pathToParent);
      if (parentFileEntry == null) {
        throw new NotFoundException(
            "The parent folder with path " + pathToParent + " does not exist.");
      }
    }

    final RegisteredProject project = projectManager.createProject(projectConfig, options);
    final ProjectConfigDto configDto = asDto(project);

    eventService.publish(new ProjectCreatedEvent(workspace, project.getPath()));

    // TODO this throws NPE
    // logProjectCreatedEvent(configDto.getName(), configDto.getProjectType());

    return injectProjectLinks(configDto);
  }

  @POST
  @Path("/batch")
  @Consumes(MediaType.APPLICATION_JSON)
  @Produces(MediaType.APPLICATION_JSON)
  @ApiOperation(
    value = "Creates batch of projects according to their configurations",
    notes =
        "A project will be created by importing when project configuration contains source object. "
            + "For creating a project by generator options should be specified.",
    response = ProjectConfigDto.class
  )
  @ApiResponses({
    @ApiResponse(code = 200, message = "OK"),
    @ApiResponse(code = 400, message = "Path for new project should be defined"),
    @ApiResponse(code = 403, message = "Operation is forbidden"),
    @ApiResponse(code = 409, message = "Project with specified name already exist in workspace"),
    @ApiResponse(code = 500, message = "Server error")
  })
  @GenerateLink(rel = LINK_REL_CREATE_BATCH_PROJECTS)
  public List<ProjectConfigDto> createBatchProjects(
      @Description("list of descriptors for projects") List<NewProjectConfigDto> projectConfigList,
      @ApiParam(value = "Force rewrite existing project", allowableValues = "true,false")
          @QueryParam("force")
          boolean rewrite)
      throws ConflictException, ForbiddenException, ServerException, NotFoundException, IOException,
          UnauthorizedException, BadRequestException {

    List<ProjectConfigDto> result = new ArrayList<>(projectConfigList.size());
    final ProjectOutputLineConsumerFactory outputOutputConsumerFactory =
        new ProjectOutputLineConsumerFactory(workspace, 300);

    for (RegisteredProject registeredProject :
        projectManager.createBatchProjects(
            projectConfigList, rewrite, outputOutputConsumerFactory)) {

      ProjectConfigDto projectConfig = injectProjectLinks(asDto(registeredProject));
      result.add(projectConfig);

      eventService.publish(new ProjectCreatedEvent(workspace, registeredProject.getPath()));
    }
    return result;
  }

  @PUT
  @Path("/{path:.*}")
  @Consumes(MediaType.APPLICATION_JSON)
  @Produces(MediaType.APPLICATION_JSON)
  @ApiOperation(value = "Updates existing project", response = ProjectConfigDto.class)
  @ApiResponses({
    @ApiResponse(code = 200, message = "OK"),
    @ApiResponse(code = 404, message = "Project with specified path doesn't exist in workspace"),
    @ApiResponse(code = 403, message = "Operation is forbidden"),
    @ApiResponse(code = 409, message = "Update operation causes conflicts"),
    @ApiResponse(code = 500, message = "Server error")
  })
  public ProjectConfigDto updateProject(
      @ApiParam(value = "Path to updated project", required = true) @PathParam("path") String path,
      ProjectConfigDto projectConfigDto)
      throws NotFoundException, ConflictException, ForbiddenException, ServerException,
          IOException {
    if (path != null) {
      projectConfigDto.setPath(path);
    }

    return asDto(projectManager.updateProject(projectConfigDto));
  }

  @DELETE
  @Path("/{path:.*}")
  @ApiOperation(
    value = "Delete a resource",
    notes =
        "Delete resources. If you want to delete a single project, specify project name. If a folder or file needs to "
            + "be deleted a path to the requested resource needs to be specified"
  )
  @ApiResponses({
    @ApiResponse(code = 204, message = ""),
    @ApiResponse(code = 403, message = "User not authorized to call this operation"),
    @ApiResponse(code = 404, message = "Not found"),
    @ApiResponse(code = 500, message = "Internal Server Error")
  })
  public void delete(@ApiParam("Path to a resource to be deleted") @PathParam("path") String path)
      throws NotFoundException, ForbiddenException, ConflictException, ServerException {
    projectManager.delete(path);
  }

  @GET
  @Path("/estimate/{path:.*}")
  @Produces(MediaType.APPLICATION_JSON)
  @ApiOperation(
    value = "Estimates if the folder supposed to be project of certain type",
    response = Map.class
  )
  @ApiResponses({
    @ApiResponse(code = 200, message = "OK"),
    @ApiResponse(code = 404, message = "Project with specified path doesn't exist in workspace"),
    @ApiResponse(code = 403, message = "Access to requested project is forbidden"),
    @ApiResponse(code = 500, message = "Server error")
  })
  public SourceEstimation estimateProject(
      @ApiParam(value = "Path to requested project", required = true) @PathParam("path")
          String path,
      @ApiParam(value = "Project Type ID to estimate against", required = true) @QueryParam("type")
          String projectType)
      throws NotFoundException, ForbiddenException, ServerException, ConflictException {
    final ProjectTypeResolution resolution = projectManager.estimateProject(path, projectType);

    final HashMap<String, List<String>> attributes = new HashMap<>();
    for (Map.Entry<String, Value> attr : resolution.getProvidedAttributes().entrySet()) {
      attributes.put(attr.getKey(), attr.getValue().getList());
    }

    return DtoFactory.newDto(SourceEstimation.class)
        .withType(projectType)
        .withMatched(resolution.matched())
        .withResolution(resolution.getResolution())
        .withAttributes(attributes);
  }

  @GET
  @Path("/resolve/{path:.*}")
  @Produces(MediaType.APPLICATION_JSON)
  public List<SourceEstimation> resolveSources(
      @ApiParam(value = "Path to requested project", required = true) @PathParam("path")
          String path)
      throws NotFoundException, ForbiddenException, ServerException, ConflictException {
    List<SourceEstimation> estimations = new ArrayList<>();
    for (ProjectTypeResolution resolution : projectManager.resolveSources(path, false)) {
      if (resolution.matched()) {
        final HashMap<String, List<String>> attributes = new HashMap<>();
        for (Map.Entry<String, Value> attr : resolution.getProvidedAttributes().entrySet()) {
          attributes.put(attr.getKey(), attr.getValue().getList());
        }
        estimations.add(
            DtoFactory.newDto(SourceEstimation.class)
                .withType(resolution.getType())
                .withMatched(resolution.matched())
                .withAttributes(attributes));
      }
    }

    return estimations;
  }

  @POST
  @Path("/import/{path:.*}")
  @Consumes(MediaType.APPLICATION_JSON)
  @Produces(MediaType.APPLICATION_JSON)
  @ApiOperation(
    value = "Import resource",
    notes =
        "Import resource. JSON with a designated importer and project location is sent. It is possible to import from "
            + "VCS or ZIP"
  )
  @ApiResponses({
    @ApiResponse(code = 204, message = ""),
    @ApiResponse(code = 401, message = "User not authorized to call this operation"),
    @ApiResponse(code = 403, message = "Forbidden operation"),
    @ApiResponse(code = 409, message = "Resource already exists"),
    @ApiResponse(code = 500, message = "Unsupported source type")
  })
  public void importProject(
      @ApiParam(value = "Path in the project", required = true) @PathParam("path") String path,
      @ApiParam(value = "Force rewrite existing project", allowableValues = "true,false")
          @QueryParam("force")
          boolean force,
      SourceStorageDto sourceStorage)
      throws ConflictException, ForbiddenException, UnauthorizedException, IOException,
          ServerException, NotFoundException, BadRequestException {

    final int delayBetweenMessages = 300;

    final ProjectImportOutputWSLineConsumer wsLineConsumer =
        new ProjectImportOutputWSLineConsumer(path, delayBetweenMessages);

    final ProjectImportOutputJsonRpcLineConsumer rpcLineConsumer =
        new ProjectImportOutputJsonRpcLineConsumer(
            path, transmitter, projectImportHandlerRegistrar, delayBetweenMessages);

    projectManager.importProject(
        path,
        sourceStorage,
        force,
        () -> new CompositeLineConsumer(wsLineConsumer, rpcLineConsumer));
  }

  @POST
  @Path("/file/{parent:.*}")
  @Consumes({MediaType.MEDIA_TYPE_WILDCARD})
  @Produces({MediaType.APPLICATION_JSON})
  @ApiOperation(
    value = "Create file",
    notes =
        "Create a new file in a project. If file type isn't specified the server will resolve its type."
  )
  @ApiResponses({
    @ApiResponse(code = 201, message = ""),
    @ApiResponse(code = 403, message = "User not authorized to call this operation"),
    @ApiResponse(code = 404, message = "Not found"),
    @ApiResponse(code = 409, message = "File already exists"),
    @ApiResponse(code = 500, message = "Internal Server Error")
  })
  public Response createFile(
      @ApiParam(value = "Path to a target directory", required = true) @PathParam("parent")
          String parentPath,
      @ApiParam(value = "New file name", required = true) @QueryParam("name") String fileName,
      InputStream content)
      throws NotFoundException, ConflictException, ForbiddenException, ServerException {
    final FolderEntry parent = projectManager.asFolder(parentPath);

    if (parent == null) {
      throw new NotFoundException("Parent not found for " + parentPath);
    }

    final FileEntry newFile = parent.createFile(fileName, content);

    eventService.publish(
        new ProjectItemModifiedEvent(
            ProjectItemModifiedEvent.EventType.CREATED,
            workspace,
            newFile.getProject(),
            newFile.getPath().toString(),
            false));

    final URI location =
        getServiceContext()
            .getServiceUriBuilder()
            .clone()
            .path(getClass(), "getFile")
            .build(new String[] {newFile.getPath().toString().substring(1)}, false);
    return Response.created(location)
        .entity(injectFileLinks(vcsStatusInjector.injectVcsStatus(asDto(newFile))))
        .build();
  }

  @POST
  @Path("/folder/{path:.*}")
  @Produces({MediaType.APPLICATION_JSON})
  @ApiOperation(value = "Create a folder", notes = "Create a folder is a specified project")
  @ApiResponses({
    @ApiResponse(code = 201, message = ""),
    @ApiResponse(code = 403, message = "User not authorized to call this operation"),
    @ApiResponse(code = 404, message = "Not found"),
    @ApiResponse(code = 409, message = "File already exists"),
    @ApiResponse(code = 500, message = "Internal Server Error")
  })
  public Response createFolder(
      @ApiParam(value = "Path to a new folder destination", required = true) @PathParam("path")
          String path)
      throws ConflictException, ForbiddenException, ServerException, NotFoundException {
    final FolderEntry newFolder = projectManager.getProjectsRoot().createFolder(path);
    final URI location =
        getServiceContext()
            .getServiceUriBuilder()
            .clone()
            .path(getClass(), "getChildren")
            .build(new String[] {newFolder.getPath().toString().substring(1)}, false);

    eventService.publish(
        new ProjectItemModifiedEvent(
            ProjectItemModifiedEvent.EventType.CREATED,
            workspace,
            newFolder.getProject(),
            newFolder.getPath().toString(),
            true));

    return Response.created(location).entity(injectFolderLinks(asDto(newFolder))).build();
  }

  @POST
  @Path("/uploadfile/{parent:.*}")
  @Consumes({MediaType.MULTIPART_FORM_DATA})
  @Produces({MediaType.TEXT_HTML})
  @ApiOperation(value = "Upload a file", notes = "Upload a new file")
  @ApiResponses({
    @ApiResponse(code = 201, message = ""),
    @ApiResponse(code = 403, message = "User not authorized to call this operation"),
    @ApiResponse(code = 404, message = "Not found"),
    @ApiResponse(code = 409, message = "File already exists"),
    @ApiResponse(code = 500, message = "Internal Server Error")
  })
  public Response uploadFile(
      @ApiParam(value = "Destination path", required = true) @PathParam("parent") String parentPath,
      Iterator<FileItem> formData)
      throws NotFoundException, ConflictException, ForbiddenException, ServerException {
    final FolderEntry parent = projectManager.asFolder(parentPath);

    if (parent == null) {
      throw new NotFoundException("Parent not found for " + parentPath);
    }

    return uploadFile(parent.getVirtualFile(), formData);
  }

  @POST
  @Path("/upload/zipfolder/{path:.*}")
  @Consumes({MediaType.MULTIPART_FORM_DATA})
  @Produces(MediaType.APPLICATION_JSON)
  @ApiOperation(
    value = "Upload zip folder",
    notes = "Upload folder from local zip",
    response = Response.class
  )
  @ApiResponses({
    @ApiResponse(code = 200, message = ""),
    @ApiResponse(code = 401, message = "User not authorized to call this operation"),
    @ApiResponse(code = 403, message = "Forbidden operation"),
    @ApiResponse(code = 404, message = "Not found"),
    @ApiResponse(code = 409, message = "Resource already exists"),
    @ApiResponse(code = 500, message = "Internal Server Error")
  })
  public Response uploadFolderFromZip(
      @ApiParam(value = "Path in the project", required = true) @PathParam("path") String path,
      Iterator<FileItem> formData)
      throws ServerException, ConflictException, ForbiddenException, NotFoundException {
    final FolderEntry parent = projectManager.asFolder(path);

    if (parent == null) {
      throw new NotFoundException("Parent not found for " + path);
    }

    return uploadZip(parent.getVirtualFile(), formData);
  }

  @ApiOperation(value = "Get file content", notes = "Get file content by its name")
  @ApiResponses({
    @ApiResponse(code = 200, message = "OK"),
    @ApiResponse(code = 403, message = "User not authorized to call this operation"),
    @ApiResponse(code = 404, message = "Not found"),
    @ApiResponse(code = 500, message = "Internal Server Error")
  })
  @GET
  @Path("/file/{path:.*}")
  public Response getFile(
      @ApiParam(value = "Path to a file", required = true) @PathParam("path") String path)
      throws IOException, NotFoundException, ForbiddenException, ServerException {
    final FileEntry file = projectManager.asFile(path);
    if (file == null) {
      throw new NotFoundException("File not found for " + path);
    }
    return Response.ok()
        .entity(file.getInputStream())
        .type(getTIKA().detect(file.getName()))
        .build();
  }

  @PUT
  @Path("/file/{path:.*}")
  @Consumes({MediaType.MEDIA_TYPE_WILDCARD})
  @ApiOperation(value = "Update file", notes = "Update an existing file with new content")
  @ApiResponses({
    @ApiResponse(code = 200, message = ""),
    @ApiResponse(code = 403, message = "User not authorized to call this operation"),
    @ApiResponse(code = 404, message = "Not found"),
    @ApiResponse(code = 500, message = "Internal Server Error")
  })
  public Response updateFile(
      @ApiParam(value = "Full path to a file", required = true) @PathParam("path") String path,
      InputStream content)
      throws NotFoundException, ForbiddenException, ServerException {
    final FileEntry file = projectManager.asFile(path);

    if (file == null) {
      throw new NotFoundException("File not found for " + path);
    }

    file.updateContent(content);

    eventService.publish(
        new ProjectItemModifiedEvent(
            ProjectItemModifiedEvent.EventType.UPDATED,
            workspace,
            file.getProject(),
            file.getPath().toString(),
            false));

    return Response.ok().build();
  }

  @POST
  @Path("/copy/{path:.*}")
  @Consumes(MediaType.APPLICATION_JSON)
  @ApiOperation(
    value = "Copy resource",
    notes = "Copy resource to a new location which is specified in a query parameter"
  )
  @ApiResponses({
    @ApiResponse(code = 201, message = ""),
    @ApiResponse(code = 403, message = "User not authorized to call this operation"),
    @ApiResponse(code = 404, message = "Not found"),
    @ApiResponse(code = 409, message = "Resource already exists"),
    @ApiResponse(code = 500, message = "Internal Server Error")
  })
  public Response copy(
      @ApiParam("Path to a resource") @PathParam("path") String path,
      @ApiParam(value = "Path to a new location", required = true) @QueryParam("to")
          String newParent,
      CopyOptions copyOptions)
      throws NotFoundException, ForbiddenException, ConflictException, ServerException {
    final VirtualFileEntry entry = projectManager.asVirtualFileEntry(path);

    // used to indicate over write of destination
    boolean isOverWrite = false;
    // used to hold new name set in request body
    String newName = entry.getName();
    if (copyOptions != null) {
      if (copyOptions.getOverWrite() != null) {
        isOverWrite = copyOptions.getOverWrite();
      }
      if (copyOptions.getName() != null) {
        newName = copyOptions.getName();
      }
    }

    final VirtualFileEntry copy = projectManager.copyTo(path, newParent, newName, isOverWrite);

    final URI location =
        getServiceContext()
            .getServiceUriBuilder()
            .path(getClass(), copy.isFile() ? "getFile" : "getChildren")
            .build(new String[] {copy.getPath().toString().substring(1)}, false);

    if (copy.isFolder()) {
      try {
        final RegisteredProject project = projectManager.getProject(copy.getPath().toString());
        final String name = project.getName();
        final String projectType = project.getProjectType().getId();
        logProjectCreatedEvent(name, projectType);
      } catch (NotFoundException ignore) {
      }
    }

    return Response.created(location).build();
  }

  @POST
  @Path("/move/{path:.*}")
  @Consumes(MediaType.APPLICATION_JSON)
  @ApiOperation(
    value = "Move resource",
    notes = "Move resource to a new location which is specified in a query parameter"
  )
  @ApiResponses({
    @ApiResponse(code = 201, message = ""),
    @ApiResponse(code = 403, message = "User not authorized to call this operation"),
    @ApiResponse(code = 404, message = "Not found"),
    @ApiResponse(code = 409, message = "Resource already exists"),
    @ApiResponse(code = 500, message = "Internal Server Error")
  })
  public Response move(
      @ApiParam("Path to a resource to be moved") @PathParam("path") String path,
      @ApiParam("Path to a new location") @QueryParam("to") String newParent,
      MoveOptions moveOptions)
      throws NotFoundException, ForbiddenException, ConflictException, ServerException {
    final VirtualFileEntry entry = projectManager.asVirtualFileEntry(path);

    // used to indicate over write of destination
    boolean isOverWrite = false;
    // used to hold new name set in request body
    String newName = entry.getName();
    if (moveOptions != null) {
      if (moveOptions.getOverWrite() != null) {
        isOverWrite = moveOptions.getOverWrite();
      }
      if (moveOptions.getName() != null) {
        newName = moveOptions.getName();
      }
    }

    final VirtualFileEntry move = projectManager.moveTo(path, newParent, newName, isOverWrite);

    final URI location =
        getServiceContext()
            .getServiceUriBuilder()
            .path(getClass(), move.isFile() ? "getFile" : "getChildren")
            .build(new String[] {move.getPath().toString().substring(1)}, false);

    return Response.created(location).build();
  }

  @POST
  @Path("/upload/zipproject/{path:.*}")
  @Consumes({MediaType.MULTIPART_FORM_DATA})
  @Produces(MediaType.APPLICATION_JSON)
  @ApiOperation(
    value = "Upload zip project",
    notes = "Upload project from local zip",
    response = ProjectConfigDto.class
  )
  @ApiResponses({
    @ApiResponse(code = 200, message = ""),
    @ApiResponse(code = 401, message = "User not authorized to call this operation"),
    @ApiResponse(code = 403, message = "Forbidden operation"),
    @ApiResponse(code = 409, message = "Resource already exists"),
    @ApiResponse(code = 500, message = "Unsupported source type")
  })
  public List<SourceEstimation> uploadProjectFromZip(
      @ApiParam(value = "Path in the project", required = true) @PathParam("path") String path,
      @ApiParam(value = "Force rewrite existing project", allowableValues = "true,false")
          @QueryParam("force")
          boolean force,
      Iterator<FileItem> formData)
      throws ServerException, IOException, ConflictException, ForbiddenException, NotFoundException,
          BadRequestException {
    // Not all importers uses virtual file system API. In this case virtual file system API doesn't
    // get events and isn't able to set
    final FolderEntry baseProjectFolder = (FolderEntry) getVirtualFile(path, force);

    int stripNumber = 0;
    String projectName = "";
    String projectDescription = "";
    FileItem contentItem = null;

    while (formData.hasNext()) {
      FileItem item = formData.next();
      if (!item.isFormField()) {
        if (contentItem == null) {
          contentItem = item;
        } else {
          throw new ServerException(
              "More then one upload file is found but only one is expected. ");
        }
      } else {
        switch (item.getFieldName()) {
          case ("name"):
            projectName = item.getString().trim();
            break;
          case ("description"):
            projectDescription = item.getString().trim();
            break;
          case ("skipFirstLevel"):
            stripNumber = Boolean.parseBoolean(item.getString().trim()) ? 1 : 0;
            break;
        }
      }
    }

    if (contentItem == null) {
      throw new ServerException("Cannot find zip file for upload.");
    }

    try (InputStream zip = contentItem.getInputStream()) {
      baseProjectFolder.getVirtualFile().unzip(zip, true, stripNumber);
    }

    return resolveSources(path);
  }

  @POST
  @Path("/import/{path:.*}")
  @Consumes(ExtMediaType.APPLICATION_ZIP)
  @ApiOperation(value = "Import zip", notes = "Import resources as zip")
  @ApiResponses({
    @ApiResponse(code = 201, message = ""),
    @ApiResponse(code = 403, message = "User not authorized to call this operation"),
    @ApiResponse(code = 404, message = "Not found"),
    @ApiResponse(code = 409, message = "Resource already exists"),
    @ApiResponse(code = 500, message = "Internal Server Error")
  })
  public Response importZip(
      @ApiParam(value = "Path to a location (where import to?)") @PathParam("path") String path,
      InputStream zip,
      @DefaultValue("false") @QueryParam("skipFirstLevel") Boolean skipFirstLevel)
      throws NotFoundException, ConflictException, ForbiddenException, ServerException {
    final FolderEntry parent = projectManager.asFolder(path);

    if (parent == null) {
      throw new NotFoundException("Parent not found for " + path);
    }

    importZip(parent.getVirtualFile(), zip, true, skipFirstLevel);

    try {
      final RegisteredProject project = projectManager.getProject(path);
      eventService.publish(new ProjectCreatedEvent(workspace, project.getPath()));
      final String projectType = project.getProjectType().getId();
      logProjectCreatedEvent(path, projectType);
    } catch (NotFoundException ignore) {
    }

    return Response.created(
            getServiceContext()
                .getServiceUriBuilder()
                .path(getClass(), "getChildren")
                .build(new String[] {parent.getPath().toString().substring(1)}, false))
        .build();
  }

  @GET
  @Path("/export/{path:.*}")
  @Produces(ExtMediaType.APPLICATION_ZIP)
  @ApiOperation(
    value = "Download ZIP",
    notes = "Export resource as zip. It can be an entire project or folder"
  )
  @ApiResponses({
    @ApiResponse(code = 201, message = ""),
    @ApiResponse(code = 403, message = "User not authorized to call this operation"),
    @ApiResponse(code = 404, message = "Not found"),
    @ApiResponse(code = 500, message = "Internal Server Error")
  })
  public InputStream exportZip(
      @ApiParam(value = "Path to resource to be exported") @PathParam("path") String path)
      throws NotFoundException, ForbiddenException, ServerException {

    final FolderEntry folder = projectManager.asFolder(path);

    if (folder == null) {
      throw new NotFoundException("Folder not found " + path);
    }

    return folder.getVirtualFile().zip();
  }

  @GET
  @Path("/export/file/{path:.*}")
  @Produces(MediaType.APPLICATION_OCTET_STREAM)
  public Response exportFile(
      @ApiParam(value = "Path to resource to be imported") @PathParam("path") String path)
      throws NotFoundException, ForbiddenException, ServerException {

    final FileEntry file = projectManager.asFile(path);

    if (file == null) {
      throw new NotFoundException("File not found " + path);
    }

    final VirtualFile virtualFile = file.getVirtualFile();

    return Response.ok(virtualFile.getContent(), getTIKA().detect(virtualFile.getName()))
        .lastModified(new Date(virtualFile.getLastModificationDate()))
        .header(HttpHeaders.CONTENT_LENGTH, Long.toString(virtualFile.getLength()))
        .header(
            HttpHeaders.CONTENT_DISPOSITION,
            "attachment; filename=\"" + virtualFile.getName() + '"')
        .build();
  }

  @GET
  @Path("/children/{parent:.*}")
  @Produces(MediaType.APPLICATION_JSON)
  @ApiOperation(
    value = "Get project children items",
    notes = "Request all children items for a project, such as files and folders",
    response = ItemReference.class,
    responseContainer = "List"
  )
  @ApiResponses({
    @ApiResponse(code = 200, message = "OK"),
    @ApiResponse(code = 403, message = "User not authorized to call this operation"),
    @ApiResponse(code = 404, message = "Not found"),
    @ApiResponse(code = 500, message = "Internal Server Error")
  })
  public List<ItemReference> getChildren(
      @ApiParam(value = "Path to a project", required = true) @PathParam("parent") String path)
      throws NotFoundException, ForbiddenException, ServerException {
    final FolderEntry folder = projectManager.asFolder(path);

    if (folder == null) {
      throw new NotFoundException("Parent not found for " + path);
    }

    final List<VirtualFileEntry> children = folder.getChildren();
    final ArrayList<ItemReference> result = new ArrayList<>(children.size());
    for (VirtualFileEntry child : children) {
      if (child.isFile()) {
        result.add(injectFileLinks(asDto((FileEntry) child)));
      } else {
        result.add(injectFolderLinks(asDto((FolderEntry) child)));
      }
    }

    return vcsStatusInjector.injectVcsStatus(result);
  }

  @GET
  @Path("/tree/{parent:.*}")
  @Produces(MediaType.APPLICATION_JSON)
  @ApiOperation(
    value = "Get project tree",
    notes = "Get project tree. Depth is specified in a query parameter",
    response = TreeElement.class
  )
  @ApiResponses({
    @ApiResponse(code = 200, message = "OK"),
    @ApiResponse(code = 403, message = "User not authorized to call this operation"),
    @ApiResponse(code = 404, message = "Not found"),
    @ApiResponse(code = 500, message = "Internal Server Error")
  })
  public TreeElement getTree(
      @ApiParam(value = "Path to resource. Can be project or its folders", required = true)
          @PathParam("parent")
          String path,
      @ApiParam(
            value =
                "Tree depth. This parameter can be dropped. If not specified ?depth=1 is used by default"
          )
          @DefaultValue("1")
          @QueryParam("depth")
          int depth,
      @ApiParam(
            value =
                "include children files (in addition to children folders). This parameter can be dropped"
                    + ". If not specified ?includeFiles=false is used by default"
          )
          @DefaultValue("false")
          @QueryParam("includeFiles")
          boolean includeFiles)
      throws NotFoundException, ForbiddenException, ServerException {
    final FolderEntry folder = projectManager.asFolder(path);

    if (folder == null) {
      throw new NotFoundException("Folder " + path + " was not found");
    }

    return newDto(TreeElement.class)
        .withNode(injectFolderLinks(asDto(folder)))
        .withChildren(getTree(folder, depth, includeFiles));
  }

  @GET
  @Path("/item/{path:.*}")
  @Produces(MediaType.APPLICATION_JSON)
  @ApiOperation(value = "Get file or folder", response = ItemReference.class)
  @ApiResponses({
    @ApiResponse(code = 200, message = "OK"),
    @ApiResponse(code = 403, message = "User not authorized to call this operation"),
    @ApiResponse(code = 404, message = "Not found"),
    @ApiResponse(code = 500, message = "Internal Server Error")
  })
  public ItemReference getItem(
      @ApiParam(value = "Path to resource. Can be project or its folders", required = true)
          @PathParam("path")
          String path)
      throws NotFoundException, ForbiddenException, ServerException {
    final VirtualFileEntry entry = projectManager.getProjectsRoot().getChild(path);

    if (entry == null) {
      throw new NotFoundException("Project " + path + " was not found");
    }

    if (entry.isFile()) {
      return injectFileLinks(vcsStatusInjector.injectVcsStatus(asDto((FileEntry) entry)));
    } else {
      return injectFolderLinks(asDto((FolderEntry) entry));
    }
  }

  @GET
  @Path("/search/{path:.*}")
  @Produces(MediaType.APPLICATION_JSON)
  @ApiOperation(
    value = "Search for resources",
    notes = "Search for resources applying a number of search filters as query parameters",
    response = SearchResult.class,
    responseContainer = "List"
  )
  @ApiResponses({
    @ApiResponse(code = 200, message = "OK"),
    @ApiResponse(code = 403, message = "User not authorized to call this operation"),
    @ApiResponse(code = 404, message = "Not found"),
    @ApiResponse(code = 409, message = "Conflict error"),
    @ApiResponse(code = 500, message = "Internal Server Error")
  })
  public ProjectSearchResponseDto search(
      @ApiParam(value = "Path to resource, i.e. where to search?", required = true)
          @PathParam("path")
          String path,
      @ApiParam(value = "Resource name") @QueryParam("name") String name,
      @ApiParam(value = "Search keywords") @QueryParam("text") String text,
      @ApiParam(
            value = "Maximum items to display. If this parameter is dropped, there are no limits"
          )
          @QueryParam("maxItems")
          @DefaultValue("-1")
          int maxItems,
      @ApiParam(value = "Skip count") @QueryParam("skipCount") int skipCount)
      throws NotFoundException, ForbiddenException, ConflictException, ServerException {
    final Searcher searcher;
    try {
      searcher = projectManager.getSearcher();
    } catch (NotFoundException e) {
      LOG.warn(e.getLocalizedMessage());
      return DtoFactory.newDto(ProjectSearchResponseDto.class);
    }

    if (skipCount < 0) {
      throw new ConflictException(String.format("Invalid 'skipCount' parameter: %d.", skipCount));
    }

    final QueryExpression expr =
        new QueryExpression()
            .setPath(path.startsWith("/") ? path : ('/' + path))
            .setName(name)
            .setText(text)
            .setMaxItems(maxItems)
            .setSkipCount(skipCount)
            .setIncludePositions(true);

    final SearchResult result = searcher.search(expr);
    final List<SearchResultEntry> searchResultEntries = result.getResults();
    return DtoFactory.newDto(ProjectSearchResponseDto.class)
        .withTotalHits(result.getTotalHits())
        .withItemReferences(prepareResults(searchResultEntries));
  }

  /**
   * Prepare result for client, add additional information like line number and line content where
   * found given text
   *
   * @throws ServerException
   */
  private List<SearchResultDto> prepareResults(List<SearchResultEntry> searchResultEntries)
      throws ServerException {
    List<SearchResultDto> results = new ArrayList<>(searchResultEntries.size());
    FolderEntry root = projectManager.getProjectsRoot();

    for (SearchResultEntry searchResultEntry : searchResultEntries) {
      VirtualFileEntry child = root.getChild(searchResultEntry.getFilePath());
      if (child != null && child.isFile()) {
        ItemReference itemReference = injectFileLinks(asDto((FileEntry) child));
        File file = child.getVirtualFile().toIoFile();
        List<LuceneSearcher.OffsetData> datas = searchResultEntry.getData();
        List<SearchOccurrenceDto> searchOccurrences = new ArrayList<>(datas.size());
        for (LuceneSearcher.OffsetData data : datas) {
          SearchOccurrenceDto searchOccurrenceDto =
              DtoFactory.getInstance()
                  .createDto(SearchOccurrenceDto.class)
                  .withPhrase(data.phrase)
                  .withScore(data.score)
                  .withStartOffset(data.startOffset)
                  .withEndOffset(data.endOffset)
                  .withLineNumber(data.lineNum)
                  .withLineContent(data.line);
          searchOccurrences.add(searchOccurrenceDto);
        }
        SearchResultDto searchResultDto = DtoFactory.getInstance().createDto(SearchResultDto.class);
        results.add(
            searchResultDto
                .withItemReference(itemReference)
                .withSearchOccurrences(searchOccurrences));
      }
    }
    return results;
  }

  @Inject
  private void configureProjectSearchRequestHandler(
      RequestHandlerConfigurator requestHandlerConfigurator) {
    requestHandlerConfigurator
        .newConfiguration()
        .methodName("project/search")
        .paramsAsDto(ProjectSearchRequestDto.class)
        .resultAsDto(ProjectSearchResponseDto.class)
        .withFunction(this::search);
  }

  public ProjectSearchResponseDto search(ProjectSearchRequestDto request) {
    String path = request.getPath();
    String name = request.getName();
    String text = request.getText();
    int maxItems = request.getMaxItems();
    int skipCount = request.getSkipCount();

    try {
      return search(path, name, text, maxItems, skipCount);
    } catch (ServerException | ConflictException | NotFoundException | ForbiddenException e) {
      throw new JsonRpcException(-27000, e.getMessage());
    }
  }

  private void logProjectCreatedEvent(@NotNull String projectName, @NotNull String projectType) {
    LOG.info(
        "EVENT#project-created# PROJECT#{}# TYPE#{}# WS#{}# USER#{}# PAAS#default#",
        projectName,
        projectType,
        workspace,
        EnvironmentContext.getCurrent().getSubject().getUserId());
  }

  private VirtualFileEntry getVirtualFile(String path, boolean force)
      throws ServerException, ForbiddenException, ConflictException, NotFoundException {
    final VirtualFileEntry virtualFile = projectManager.getProjectsRoot().getChild(path);
    if (virtualFile != null && virtualFile.isFile()) {
      // File with same name exist already exists.
      throw new ConflictException(String.format("File with the name '%s' already exists.", path));
    } else {
      if (virtualFile == null) {
        return projectManager.getProjectsRoot().createFolder(path);
      } else if (!force) {
        // Project already exists.
        throw new ConflictException(
            String.format("Project with the name '%s' already exists.", path));
      }
    }

    return virtualFile;
  }

  private List<TreeElement> getTree(FolderEntry folder, int depth, boolean includeFiles)
      throws ServerException, NotFoundException {
    if (depth == 0) {
      return null;
    }

    final List<? extends VirtualFileEntry> children;

    if (includeFiles) {
      children = folder.getChildFoldersFiles();
    } else {
      children = folder.getChildFolders();
    }

    final List<TreeElement> nodes = new ArrayList<>(children.size());
    for (VirtualFileEntry child : children) {
      if (child.isFolder()) {
        nodes.add(
            newDto(TreeElement.class)
                .withNode(injectFolderLinks(asDto((FolderEntry) child)))
                .withChildren(getTree((FolderEntry) child, depth - 1, includeFiles)));
      } else {
        nodes.add(newDto(TreeElement.class).withNode(injectFileLinks(asDto((FileEntry) child))));
      }
    }

    return vcsStatusInjector.injectVcsStatusTreeElements(nodes);
  }

  /* --------------------------------------------------------------------------- */
  /* TODO check "upload" methods below, they were copied from old VFS as is      */
  /* --------------------------------------------------------------------------- */

  private static Response uploadFile(VirtualFile parent, Iterator<FileItem> formData)
      throws ForbiddenException, ConflictException, ServerException {
    try {
      FileItem contentItem = null;
      String name = null;
      boolean overwrite = false;

      while (formData.hasNext()) {
        FileItem item = formData.next();
        if (!item.isFormField()) {
          if (contentItem == null) {
            contentItem = item;
          } else {
            throw new ServerException(
                "More then one upload file is found but only one should be. ");
          }
        } else if ("name".equals(item.getFieldName())) {
          name = item.getString().trim();
        } else if ("overwrite".equals(item.getFieldName())) {
          overwrite = Boolean.parseBoolean(item.getString().trim());
        }
      }

      if (contentItem == null) {
        throw new ServerException("Cannot find file for upload. ");
      }
      if (name == null || name.isEmpty()) {
        name = contentItem.getName();
      }

      try {
        try {
          parent.createFile(name, contentItem.getInputStream());
        } catch (ConflictException e) {
          if (!overwrite) {
            throw new ConflictException("Unable upload file. Item with the same name exists. ");
          }
          parent
              .getChild(org.eclipse.che.api.vfs.Path.of(name))
              .updateContent(contentItem.getInputStream(), null);
        }
      } catch (IOException ioe) {
        throw new ServerException(ioe.getMessage(), ioe);
      }

      return Response.ok("", MediaType.TEXT_HTML).build();
    } catch (ForbiddenException | ConflictException | ServerException e) {
      HtmlErrorFormatter.sendErrorAsHTML(e);
      // never thrown
      throw e;
    }
  }

  private static Response uploadZip(VirtualFile parent, Iterator<FileItem> formData)
      throws ForbiddenException, ConflictException, ServerException {
    try {
      FileItem contentItem = null;
      boolean overwrite = false;
      boolean skipFirstLevel = false;
      while (formData.hasNext()) {
        FileItem item = formData.next();
        if (!item.isFormField()) {
          if (contentItem == null) {
            contentItem = item;
          } else {
            throw new ServerException(
                "More then one upload file is found but only one should be. ");
          }
        } else if ("overwrite".equals(item.getFieldName())) {
          overwrite = Boolean.parseBoolean(item.getString().trim());
        } else if ("skipFirstLevel".equals(item.getFieldName())) {
          skipFirstLevel = Boolean.parseBoolean(item.getString().trim());
        }
      }
      if (contentItem == null) {
        throw new ServerException("Cannot find file for upload. ");
      }
      try {
        importZip(parent, contentItem.getInputStream(), overwrite, skipFirstLevel);
      } catch (IOException ioe) {
        throw new ServerException(ioe.getMessage(), ioe);
      }
      return Response.ok("", MediaType.TEXT_HTML).build();
    } catch (ForbiddenException | ConflictException | ServerException e) {
      HtmlErrorFormatter.sendErrorAsHTML(e);
      // never thrown
      throw e;
    }
  }

  private static void importZip(
      VirtualFile parent, InputStream in, boolean overwrite, boolean skipFirstLevel)
      throws ForbiddenException, ConflictException, ServerException {
    int stripNum = skipFirstLevel ? 1 : 0;
    parent.unzip(in, overwrite, stripNum);
  }

  private ItemReference injectFileLinks(ItemReference itemReference) {
    return projectServiceLinksInjector.injectFileLinks(itemReference, getServiceContext());
  }

  private ItemReference injectFolderLinks(ItemReference itemReference) {
    return projectServiceLinksInjector.injectFolderLinks(itemReference, getServiceContext());
  }

  private ProjectConfigDto injectProjectLinks(ProjectConfigDto projectConfig) {
    return projectServiceLinksInjector.injectProjectLinks(projectConfig, getServiceContext());
  }

  /** Lazy init of Tika. */
  private synchronized Tika getTIKA() {
    if (TIKA == null) {
      TIKA = new Tika();
    }
    return TIKA;
  }
}
